#!/usr/bin/env perl
#
# Version:
#    0.12
#
# Copyright:
#    Stephen Blott (smblott@gmail.com)
# 
# License:
#    MIT license
#    http://opensource.org/licenses/mit-license.php
#
# Project home:
#    - http://code.google.com/p/squeezy/
#
# See also:
#    squeezy.README.txt
#    squeezy.conf
#

use strict;

#
# imports
#

use IO::Socket;
use IO::Select;
use Cwd 'realpath';
# use Data::Dumper;

#
# find a configuration file
#

my $conf_base = 'squeezy.conf';
my $conf_locs = qq( ./$conf_base
		    $ENV{HOME}/.$conf_base
		    $ENV{HOME}/.config/squeezy/$conf_base
		    $ENV{HOME}/.config/$conf_base
		    /usr/local/etc/$conf_base
		    /etc/$conf_base );

my @conf_file = map { $_ && -r $_ ? $_ : () } split /\s+/, $conf_locs;
my $conf_file = $conf_file[0];

# die "squeezy error: could not find configuration file:\n", map { "\t$_\n" } @conf_file
#    unless $conf_file;

#
# some globals
#

my $my_name    =  "/$0";
   $my_name    =~ s:^.*/::;
my $server     = 'localhost';
my $port       =  9090;
my $username   =  undef;
my $password   =  undef;
my $group      =  undef;
my @group;
my %shortcut;
my $default    =  undef;
my $silent     =  undef;
my $little     = '3';
my $lot        = '10';
my $prefixpre  = '';
my $prefixpos  = '';
my $tick       = undef;
my $player     = undef;
my %command;
my @player;

#
# utilities
#

sub urlencode {
  my $str = shift;
  $str =~ s/([^A-Za-z0-9])/sprintf('%%%02X', ord($1))/seg;
  return $str;
}

sub urldecode {
  my $str = shift;
  $str =~ s/%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
  return $str;
}

sub pick_field
{
   return urldecode((split /\s+/, shift)[shift]);
}

sub report
{
   my $mess = shift;
   my $value = shift;
   print join(' ', ($player, $mess, &urldecode($value))), "\n";
   return $value;
}

sub terminal_cols
{
   -t STDOUT || return undef;
   return (map { chomp; $_ } `tput co`)[0];
}

#
# server socket commands
#

my $socket = undef;

sub send_command
{
   # this a syncronous command/response interaction, one line is sent to the
   # server and one line is received in response (and returned)
   if ( ! $socket )
   {
      $socket  = IO::Socket::INET -> new ( PeerAddr => $server,
		                           PeerPort => $port,
		                           Proto    => 'tcp' )
	         or
	         die "squeezy error: could not connect to $server port $port\n";

      if ( $username )
      {
	 if ( ! $password )
	    { die "squeezy error: username provided, but no password provided (username='$username')\n"; }
	 print $socket "login $username $password\n";
	 # ignore the following response; even with an incorrect
	 # username/password the server happily echos it back
	 my $response = <$socket>;
      }
      # force some unnecessary client/server interaction in order to detect
      # an authentication failure
      print $socket "player count ?\n";
      if ( ! $socket->connected() || ! <$socket> )
	 { die "squeezy error: authentication failed (username='$username', password='$password')\n"; }
   }

   if ( ! $socket->connected() )
      { die "squeezy error: server dropped socket connection\n"; }

   print $socket join(" ", @_), "\n";
   my $response = <$socket>;

   if ( ! $socket->connected() )
      { die "squeezy error: server dropped socket connection\n"; }

   chomp $response;
   return $response;
}

#
# read configuration file
#

if ( -r $conf_file )
{
   open CONFIG, "<$conf_file"
      or die "squeezy: open of configuration file failed ($conf_file)\n";

   foreach ( <CONFIG> )
   {
      s/#.*//;       # strip comments
      s/^\s*//;      # strip leading whitespace
      s/\s*$//;      # strip trailing whitespace
      $_ || do next; # skip empty lines

      my @arg = split /\s+/;

      $arg[0] eq 'server'   && $#arg == 1 && do { $server   = $arg[1]; next; };
      $arg[0] eq 'port'     && $#arg == 1 && do { $port     = $arg[1]; next; };
      $arg[0] eq 'username' && $#arg == 1 && do { $username = $arg[1]; next; };
      $arg[0] eq 'password' && $#arg == 1 && do { $password = $arg[1]; next; };

      $arg[0] eq 'player'
	 && do
	 {  if ( $#arg == 1 ) { push @{$group[0]},       $arg[1]; next; }
	    if ( $#arg == 2 ) { push @{$group[$arg[1]]}, $arg[2]; next; } };

      $arg[0] eq 'shortcut' && $#arg == 2
	 && do
	 {  $default = $arg[2] unless $default;
	    $shortcut{$arg[1]} = $arg[2];
	    next; };

      $arg[0] eq 'small_volume' && $#arg == 1 && $arg[1] =~ m/^[0-9]+$/ && do { $little = $arg[1]; next; };
      $arg[0] eq 'large_volume' && $#arg == 1 && $arg[1] =~ m/^[0-9]+$/ && do { $lot    = $arg[1]; next; };

      $arg[0] eq 'prefixpre'
	 && do
	 {  if ( $#arg == 0 ) { next; }
	    if ( $#arg == 1 ) { $prefixpre = $arg[1]; next; } };

      $arg[0] eq 'prefixpos'
	 && do
	 {  if ( $#arg == 0 ) { next; }
	    if ( $#arg == 1 ) { $prefixpos = $arg[1]; next; } };

      die "error in $conf_file: $_\n"
   }

   close CONFIG;
}
else
{
   # no configuration file; try to ask squeezeserver on localhost:9090 to ask
   # it for the names of players
   my $count = pick_field 2, send_command 'player count ?';
   if ( $count )
   {
      for (my $i=0; $i<$count; $i+=1)
      {
	 my $pn = pick_field 3, send_command "player name $i ?"; 
	 if ( $pn && $pn ne '?' )
	    { push @{$group[0]}, $pn }
	 else
	 {  my $pid = pick_field 3, send_command "player id $i ?"; 
	    if ( $pid && $pid ne '?' )
	       { push @{$group[0]}, $pid } }
      }
      if ( $group[0][0] )
      {
	 print 'players: ', join(' ', @{$group[0]}), "\n";
      }
   }
}

if ( $username && ! $password )
   { die "squeezy configuration error: username is set ($username) but password is unset\n"; }

while ( ! $player )
{
   @player = map { @$_ } @group;
   $player = $player[0];
   if ( !$player )
   {
      print STDERR "squeezy warning: no players specified\n";
      print STDERR "                 you likely won't be able to do much without them\n";
      $group[0][0] = 'NO_PLAYER_SPECIFIED_IN_CONF_FILE____THIS_WILL_NOT_WORK';
   }
}

push @group, [ @player ];

#
# strip any trailing '/'s from $prefixpre and $prefixpos
#

if ( $prefixpre ) { $prefixpre =~ s:/+$::; }
if ( $prefixpos ) { $prefixpos =~ s:/+$::; }

#
# listen output wrapper functions
#

sub listen_output
{
   # this is just an output hook, it's somewhere to put post-processing
   # triggers for the -listen output
   #
   # if we see a "new song", then request full status for that player
   #
   if ( $_[1] eq 'playlist' && $_[2] eq 'newsong' )
   {
      print $socket "$_[0] status - 1\n";
   }

   print STDOUT join(' ', @_), "\n";
}

sub listen
{
   my $count = pick_field 2, send_command 'player count ?';
   listen_output 'player', 'count', $count;

   my @ids;
   for (my $i=0; $i<$count; $i+=1)
   {
      my $id = pick_field 3, send_command "player id $i ?";
      push @ids, $id;
      listen_output 'player', 'id', $i, $id;
   }

   # the &send_command() calls above force $socket to be open by here
   my $select = IO::Select->new($socket);
   STDOUT->autoflush(1);

   sub socket_input
   {
      my $wait = shift;

      while ( $select->can_read($wait) )
      {
	 my $line = <$socket>;
	 chomp $line;
	 my @line = split(/\s+/, $line, 3);

	 if ( $line[1] eq 'status' )
	 {
	    # 'status' lines contain many fields, so parse them here to make
	    # them easier to deal with down stream
	    my @prefix = (urldecode($line[0]), 'status');
	    listen_output @prefix, 'start', 'status';
	    foreach ( map { urldecode($_) } split(/\s+/, $line) )
	       { listen_output @prefix, split(/:/, $_, 2); }
	    listen_output @prefix, 'end', 'status';
	    next;
	 }

	 listen_output map { urldecode($_) } split(/\s+/, $line);
      }
   };

   # from here on we print() directly to the socket and sprinkle in
   # socket_input() calls to catch the ouput; syncronous send_command() calls
   # will no loger work

   for (my $i=0; $i<$count; $i+=1)
   {
      print $socket "player name $ids[$i] ?\n";
      socket_input(0); # 0 here means do not block
   }

   for (my $i=0; $i<$count; $i+=1)
   {
      print $socket "$ids[$i] status - 1\n";
      socket_input(0); # 0 here means do not block
   }

   print $socket "listen 1\n";
   while ( $socket->connected() )
   {
      socket_input(1); # 1 here means block for 1 second
      if ( $tick )
      {
	 for (my $i=0; $i<$count; $i+=1)
	 {
	    print $socket "$ids[$i] time ?\n";
	    socket_input(0); # 0 here means do not block
	 }
      }
   }

   print STDERR "squeezy error: lost socket connection\n";
   $socket = undef;
}

# ######################################################################
# commands
#

sub command
{
   my $command = shift;
   $command{$command}->{command}(@_);
}

%command =
(

   '-add' =>
      {
	 help     => 'add the indicated file, directory (contents), playlist or url to the playlist',
	 do_shift => 1,
	 command  => sub { command -play, shift, 'add'; }
      },

   '-all' =>
      {
	 help     => 'apply subsequent commands to all players (eg. squeezy -all -off)',
	 command  => sub { $group = $#group; }
      },

   '-button' =>
      {
	 help     => 'send a button key command to the player (see the server\'s "Default.map" file)',
	 do_shift => 1,
	 command  => sub { send_command $player, 'button', $_[0]; }
      },

   '-default' =>
      {
	 help    => "play the default shortcut (the first one in the configuration file, currently $default)",
	 command =>
	    sub
	    {
	       if ( ! $default )
		  { die 'squeezy -default: no shortcuts in configuration file, so no default available\n'; }
	       command -play, $default;
	    }
      },

   '-die_if_playing' =>
      {
	 help    => 'if this player is on and playing, then die immediately; exit code 1',
	 command =>
	    sub { if ( command('-power') && command('-mode') eq 'play' )
		     { print "$player is on and playing, exiting immediately\n"; exit(1); } }
      },

   '-elapsed' =>
      {
	 help    => 'show the elapsed time in currently-playing track/url',
	 command => sub { report 'time', pick_field 2, send_command "$player time ?"; }
      },

   '-exit_if_playing' =>
      {
	 help    => 'if this player is on and playing, then exit immediately; exit code 0',
	 command =>
	    sub { if ( command('-power') && command('-mode') eq 'play' )
		     { print "$player is on and playing, exiting immediately\n"; exit(0); } }
      },

   '-exit_if_sleeping' =>
      {
	 help    => 'if this player is on and is timing down to sleep, then exit immediately; exit code 0',
	 command =>
	    sub {
	       if ( command('-power') && command('-sleeping') )
		     { print "$player is on and timing down to sleep, exiting immediately\n"; exit(0); } }
      },

   '-group' =>
      {
	 help     => 'apply subsequent command to all players in this group (eg. squeezy -group 1)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       if ( ! ($_[0] =~ m/^[0-9]+$/ ) )
		  { die "squeezy -group: not a number ($_[0])\n"; }
	       if ( $#group < $_[0] )
		  { die "squeezy -group: invalid group number ($_[0], max is $#group)\n"; }
	       $group = shift;
	    }
      },

   '-groups' =>
      {
	 help     => 'show the configured player groups',
	 command  =>
	    sub
	    {
	       for (my $i=0; $i<=$#group; $i+=1)
	       {
		  print "group $i:\n";
		  foreach my $g ( @{$group[$i]} )
		     { print "   $g\n"; }
	       }
	    }
      },

   '-help' =>
      {
	 help    => 'show this help message',
	 command =>
	    sub
	    {
	       print "configuration file:\n   $conf_file\n";
	       print "\nconfiguration file search locations:\n";
	       foreach ( map { $_ ? $_ : () } split /\s+/, $conf_locs )
		  { print "   $_\n"; }
	       print "\ncommand-line options:\n";
	       foreach my $command ( sort keys %command )
	       {
		  if ( $command{$command}->{help} )
		  {
		     my $argument = $command{$command}->{do_shift} ? ' <arg>' : '';
		     print sprintf "%-18s: %s\n", "$command$argument", $command{$command}->{help};
		  }
	       }
	    }
      },

   '-jump' =>
      {
	 help     => 'jump to a track on the current playlist (relative or absolute, eg. 3, +2, -4)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $target = shift;
	       if ( ! ( $target =~ m/^[+-]?[0-9]{1,}$/ ) )
		  { die "squeezy -jump: invalid jump specifier ($target)\n"; }
	       my $count = command -playlist_length;
	       my $index = command -playlist_index;
	       report 'new track', pick_field 3, send_command "$player playlist index $target";
	       command -playing;
	    }
      },

   '-listen' =>
      {
	 help     => 'listen to squeezeserver activity (on standard output, see also \'-tick\')',
	 command  => sub { &listen(); }
      },

   '-louder' =>
      {
	 help    => "increase the volume slightly (by $little%)",
	 command => sub { command -volume, "+$little"; }
      },

   '-Louder' =>
      {
	 help    => "increase the volume significantly (by $lot%)",
	 command => sub { command -volume, "+$lot" }
      },

   '-mode' =>
      {
	 help    => 'show the player\'s mode ("play", "stop" or "pause")',
	 command =>
	    sub
	    {
	       command -power;
	       report 'mode', pick_field 2, send_command "$player mode ?";
	    }
      },

   '-mute' =>
      {
	 # this is not real "muting";  it should be possible subsequently to
	 # unmute, returning to the original volume;  this doesn't work on
	 # my players (perhps because they're old), so I took the brute
	 # force approach here --> just set the volume to 0
	 help     => 'set volume to 0',
	 command  => sub { command -volume, '0'; }
      },

   '-next' =>
      {
	 help    => 'jump to the next track on the playlist',
	 command =>
	    sub
	    {
	       send_command "$player playlist index +1";
	       command -playing;
	    }
      },

   '-on' =>
      {
	 help    => 'turn player on and start playing the current playlist/track',
	 command =>
	    sub
	    {
	       send_command "$player power 1";
	       send_command "$player pause 0";
	       command -power;
	       command -mode;
	       command -playing;
	    }
      },

   '-off' =>
      {
	 help    => 'turn player off',
	 command =>
	    sub
	    {
	       send_command "$player power 0";;
	       command -power
	    }
      },

   '-options' =>
      {
	 help    => 'list all options to standard output (useful for configuring shell completion)',
	 command => sub { print join(' ', sort keys %command), "\n"; }
      },

   '-pause' =>
      {
	 help    => 'toggle pause',
	 command =>
	    sub
	    {
	       send_command "$player pause";
	       command -mode;
	    }
      },

   '-play' =>
      {
	 help     => 'play the indicated file, directory (contents), playlist or url',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $play = shift;
	       my $oper = shift || 'play'; # can also be 'add'
	       if ( -r $play )
	       {
		  $play = realpath($play);
		  $play =~ s:^$prefixpre::; # $play should still have a leading '/' here
		  $play = "$prefixpos$play";
	       }
	       $play = urlencode $play;
	       send_command "$player playlist $oper $play";
	       command -playing;
	    }
      },

   '-player_count' =>
      {
	 help    => 'show the number of connected players',
	 command  => sub { report 'player count', pick_field 2, send_command 'player count ?'; }
      },

   '-player_id' =>
      {
	 help     => 'show the id of the player',
	 command  => sub { report 'player id',  pick_field 3, send_command "player id $player ?"; }
      },

   '-player_ip' =>
      {
	 help     => 'show the ip address of the player',
	 command  => sub { report 'player IP',  pick_field 3, send_command "player ip $player ?"; }
      },

   '-playlist' =>
      {
	 help     => 'show the current playlist',
	 command  =>
	    sub
	    {
	       my $count = command -playlist_length;
	       my $index = command -playlist_index;
	       my $cols  = terminal_cols();
	       for (my $i=0; $i<$count; $i+= 1)
	       {
		  my $album  = pick_field 4, send_command "$player playlist album  $i ?";
		  my $artist = pick_field 4, send_command "$player playlist artist $i ?";
		  my $title  = pick_field 4, send_command "$player playlist title  $i ?";
		  if ( $cols )
		  {
		     my $left  = sprintf "%s %-3d %s", $i == $index ? '>' : ' ', $i, $title;
		     my $right = sprintf "%${cols}s", "$album - $artist";
		     my $line  = sprintf "%s %s", $left, substr($right, length($left)+1);
		     print substr($line,0,$cols), "\n";
		  }
		  else
		  {
		     print $i == $index ? '> ' : '  ';
		     print sprintf "%-3d ", $i;
		     print "$title; $album - $artist\n";
		  }
	       }
	    }
      },

   '-playlist_delete' =>
      {
	 help     => 'delete (or remove) an item from the current playlist (eg. \'-playlist_delete 3\')',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $target = shift;
	       if ( ! ( $target =~ m/^[0-9]{1,}$/ ) )
		  { die "squeezy -playlist_delete: invalid playlist specifier ($target)\n"; }
	       my $count = command -playlist_length;
	       if ( $count <= $target )
		  { die "squeezy -playlist_delete: invalid playlist specifier, too big ($target)\n"; }
	       report 'playlist delete', pick_field 3, send_command "$player playlist delete $target";
	       command -playing;
	    }
      },

   '-playlist_index' =>
      {
	 help     => 'show the index of the current song in the playlist',
	 command  => sub { report 'playlist index', pick_field 3, send_command "$player playlist index ?"; }
      },

   '-playlist_length' =>
      {
	 help     => 'show the length of the current playlist',
	 command  => sub { report 'playlist length', pick_field 3, send_command "$player playlist tracks ?"; }
      },

   '-player_model' =>
      {
	 help     => 'show the model of the player',
	 command  => sub { report 'player model',  pick_field 3, send_command "player model $player ?"; }
      },

   '-players' =>
      {
	 help    => 'show all configured players',
	 command => sub { print map { "$_\n" } @player; }
      },

   '-playing' =>
      {
	 help    => 'show the currently-playing track',
	 command => sub { report 'playing', pick_field 2, send_command "$player current_title ?"; }
      },

   '-power' =>
      {
	 help    => 'show whether power is on or off',
	 command => sub { report 'power', pick_field 2, send_command "$player power ?"; }
      },

   '-previous' =>
      {
	 help    => 'jump to the previous track on the playlist',
	 command =>
	    sub
	    {
	       send_command "$player playlist index -1";
	       command -playing;
	    }
      },

   '-print_links' =>
      {
	 help    => 'show a list of commands suitable for creating player pseudonyms for the squeezy command',
	 command =>
	    sub
	    {
	       my $squeezy = $0; # not $my_name
	       my $dir     = $squeezy;
		  $dir     =~ s:/[^/]+$::;
	       foreach ( map { lc } @player )
		  { print "ln -vf \"$squeezy\" \"$dir/$_\"\n"; }
	    }
      },

   '-quieter' =>
      {
	 help    => "decrease the volume slightly (by $little%)",
	 command => sub { command -volume, "-$little"; }
      },

   '-Quieter' =>
      {
	 help    => "reduce the volume significantly (by $lot%)",
	 command => sub { command -volume, "-$lot"; }
      },

   '-shortcuts' =>
      {
	 help     => 'show the configured shortcuts',
	 command  => sub { foreach ( keys %shortcut )
		              { print sprintf "%-8s %s\n", $_, $shortcut{$_}; }
	    }
      },

   '-show' =>
      {
	 help    => "show a message on the player's display (for six seconds)",
	 do_shift => 1,
	 command  => sub { my $show = urlencode shift;
	                   send_command "$player show font:huge duration:6 centered:1 line2:$show"; }
      },

   '-silent' =>
      {
	 help     => 'redirect standard output to \'/dev/null\'',
	 command  => sub { open STDOUT, '>', '/dev/null'; }
      },

   '-sleep' =>
      {
	 help     => 'make the player sleep in <arg> minutes (use \'0\' or \'-sleep_clear\' to cancel sleep)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $time = shift;
	       if ( ! ( $time =~ m/^[0-9]{1,}$/ ) )
		  { die "squeezy -sleep: invalid argument ($time)\n"; }
	       $time = 60 * $time;
	       send_command "$player sleep $time";
	       command -sleeping;
	    }
      },

   '-sleep_clear' =>
      {
	 help     => 'cancel sleep',
	 command  =>
	    sub
	    {
	       send_command "$player sleep 0";
	       command -sleeping;
	    }
      },

   '-sleeping' =>
      {
	 help     => 'report on a player\'s sleep status (indicated in seconds)',
	 command  =>
	    sub
	    {
	       my $time = pick_field 2, send_command "$player sleep ?";
	       my @time = split('\.', $time);
	       report 'sleep', $time[0];
	    }
      },

   '-sync' =>
      {
	 help     => 'synchronise player with <arg> (another player; unreliable)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $arg = shift;
	       send_command "$player sync $arg";
	       report 'sync', pick_field 2, send_command "$player sync ?";
	    }
      },

   '-syncgroups' =>
      {
	 help     => 'show synchronisation groups',
	 command  => sub { report 'syncgroups', pick_field 2, send_command 'syncgroups ?'; }
      },

   '-tick' =>
      {
	 help    => 'make -listen request all players\' playback time every second',
	 command => sub { $tick = 1; }
      },

   '-time' =>
      {
	 help     => 'jump to an absolute or relative position in the current track (measured in seconds; eg. 10, +30, -30)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $time = shift;
	       if ( ! ( $time =~ m/^[+-]?[0-9]{1,}$/ ) )
		  { die "squeezy -time: invalid time specifier ($time)\n"; }
	       report 'time', pick_field 2, send_command "$player time $time";
	    }
      },

   '-unsync' =>
      {
	 help    => 'unsynchronise player (unreliable)',
	 command =>
	    sub
	    {
	       send_command "$player sync -";
	       command -syncgroups;
	       report 'sync', pick_field 2, send_command "$player sync ?";
	    }
      },

   '-volume' =>
      {
	 help    => "set the player's volume (absolute or relative, use \'?\' to query the current volume)",
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $vol = shift;
	       if ( $vol ne '?' )
	       {
		  if ( ! ( $vol =~ m/^[+-]?[0-9]{1,}$/ ) )
		     { die "squeezy -volume: invalid volume specifier ($vol)\n"; }
		  if ( 100 < $vol || $vol < -100)
		     { die "squeezy -volume: invalid volume specifier ($vol, should be min/max 100)\n"; }
		  send_command "$player mixer volume $vol";
	       }
	       report 'volume', pick_field 3, send_command "$player mixer volume ?";
	    }
      },

);

#
# add options to select each player; for example, for a player 'Kitchen' this
# will add options:

#   -Kitchen
#   -kitchen
#   -k
#
# (options are not added if they would overwrite existing options; so, if you
#  have a player named 'Help', then an option '-help' will not be added)
#

foreach ( @player )
{
   my $p  =    $_;            # player name
   my $lc = lc $p;            # same, but lower case
   my $fc = substr($lc,0,1);  # just the first character

   foreach ( $_, $lc, $fc )
   {
      if ( ! $command{$_} )
      {
	 $command{"-$_"} = { is_player => 1,
			     help      => "select player $p (automatically added option)",
			     command   => sub { $group  = undef; $player = $p; print "$p selected\n"; }, };
      }
   }
}

#
# add a command to directly play each shortcut; again, only if it doesn't
# overwrite an existing command
#

foreach ( keys %shortcut )
{
   if ( ! $command{-$_} )
   {
      my $sc = $_;
      $command{-$sc} =
	 {
	    help    => "play shortcut $sc ($shortcut{$sc}, automatically added option)",
	    command => sub { command -play, $shortcut{$sc}; }
	 };
   }
}

#
# some abbreviated/alternative option names
#

$command{'-'}              = $command{-quieter}          if ! $command{'-'};
$command{'--'}             = $command{-Quieter}          if ! $command{'--'};
$command{'+'}              = $command{-louder}           if ! $command{'+'};
$command{'++'}             = $command{-Louder}           if ! $command{'++'};
$command{-currenttitle}    = $command{-playing}          if ! $command{-currenttitle};
$command{-playlist_remove} = $command{-playlist_delete}  if ! $command{-playlist_remove};
$command{-title}           = $command{-playing}          if ! $command{-title};
$command{-prev}            = $command{-previous}         if ! $command{-prev};
$command{-goto}            = $command{-jump}             if ! $command{-goto};
$command{'--help'}         = $command{-help}             if ! $command{'--help'};

#
# if the name of this "executable" is also the name of a player, then select
# that player as the default player
#
# with a player named 'Kitchen' (say) and an appropriate hard link:
#
#   ln -v /usr/local/bin/squeezy /usr/local/bin/kitchen
#
# this allows you to say things like "kitchen -quieter"; see also the
# -print_links option
#

if ( $command{-$my_name} && $command{-$my_name}{is_player} )
   { command -$my_name; }

#
# argument processing (the main loop)
#

while ( 0 <= $#ARGV )
{
   my $arg  = shift @ARGV;
   my @whom = defined $group ? @{$group[$group]} : ( $player );

   if ( $command{$arg} )
   {
      #
      # argument checking (to the extent that that's possible here)
      #

      if ( $command{$arg}->{do_shift} )
      {
	 my $argument = $ARGV[0];
	 if ( ! defined $argument )
	    { die "squeezy $arg: no argument where argument required\n"; }
	 if ( $command{$argument} )
	    { die "squeezy $arg: invalid argument ($argument, which rather looks like an option)\n"; }
      }

      #
      # command processing
      #

      foreach my $p ( @whom )
      {
	 $player = $p;
	 command $arg, $ARGV[0];
      }
      
      if ( $command{$arg}->{do_shift} )
	 { shift @ARGV; }

      next;
   }

   die "squeezy error: unknown argument ($arg)\n";
}

exit(0);

